Czas wreszcie przetestować działanie pakietu pickle!
Skrypt z najlepszym znalezionym przez moją grupę modelem (XGBoost z tuningiem hyperparametrów) umieściłem w pliku xgboost_model.ipynb.
import pandas as pd
import numpy as np
import math
import pickle
from sklearn.model_selection import train_test_split
import dalex as dx
import lime
Klasycznie - czerwonego wina czas 🍷
df_wines = pd.read_csv('winequality-red.csv')
df_wines["is_good"] = df_wines.apply(lambda row: 1 if row.quality > 5 else 0, axis = 1)
X = df_wines.drop(["is_good", 'quality'], axis=1)
y = df_wines["is_good"]
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,random_state = 42)
y_test = y_test.reset_index()["is_good"]
xgb = pickle.load(open("xgb", 'rb'))
y_pred = xgb.predict(X_test)
print("Accuracy: ", sum(y_test == y_pred)/y_test.shape[0]*100, "%", sep = "")
Accuracy: 78.5%
Sukces!!!
Zainicjujmy naszego wytłumaczacza.
explainer = dx.Explainer(xgb, X_train, y_train, label = "Random Forest")
Preparation of a new explainer is initiated -> data : 1199 rows 11 cols -> target variable : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray. -> target variable : 1199 values -> model_class : sklearn.model_selection._search.RandomizedSearchCV (default) -> label : Random Forest -> predict function : <function yhat_proba_default at 0x000001DCF8670AF0> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 0.00364, mean = 0.535, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -0.626, mean = -2.74e-05, max = 0.775 -> model_info : package sklearn A new explainer has been created!
Ostatnio wziąłem pod lupę potęgi dwójki, dziś czas może na potęgi trójki. Jak sprawa wygląda z $3^{3}$-cią obserwacją ze zbioru testowego?
X_test.iloc[26,:]
fixed acidity 6.80000 volatile acidity 0.56000 citric acid 0.22000 residual sugar 1.80000 chlorides 0.07400 free sulfur dioxide 15.00000 total sulfur dioxide 24.00000 density 0.99438 pH 3.40000 sulphates 0.82000 alcohol 11.20000 Name: 859, dtype: float64
explainer.predict(X_test)[26]
0.9651078
y_test[26]
1
W tym wypadku nasz model przewidział wino dobre z prawdopodobieństwem aż 96,5% i rzeczywiście miał rację!
ps = explainer.predict_surrogate(X_test.iloc[26,:], type = "lime")
ps.show_in_notebook()
Bardzo wysokie prawdopodobieństwo pozytywnej oceny wina widać na zestawienu wyżej. Zczytując od góry najbardziej kluczowe, mamy tu styczność z:
Wszystkie powyższe cechy zaowocowały +10% wpływem na ocenę modelu. Pozostałe 6 kolumn miało już o wiele mniejszy wpływ - wszystkie do 5%.
Co ciekawe, tylko dwie najmniej znaczące cechy (wskaźnik pH oraz zawartość kwasu cytrynowego) nie były ani w 25% najmniejszych wartości, ani wśród 25% największych. Interesujące, że tak mało ich, jednak z drugiej strony mniej dziwne, że znalazły się one na samym dnie - naturalnie, że z kolumn o wartościach bardziej oddalonych od "najbardziej typowych", da się więcej odczytać i przewidywać.
Można zauważyć także dużo wspólnych własności wygenerowanej wyżej dekompozycji z macierzą korelacji utworzonej podczas EDA - dodatnia korelacja i wartość powyżej mediany oraz ujemna korelacja wraz z cechą poniżej 50% obserwacji owocują w pewniejsze przewidywanie wina jako dobrego, zaś pozostałe dwa parowane przypadki - wręcz przeciwnie.
Niech żyją potęgi trójki!!
indexes = [3**x-1 for x in range(0, round(math.log(X_test.shape[0])))]
indexes
[0, 2, 8, 26, 80, 242]
Czy wszystkie z owych sześciu win zostało dobrze zakwalifikowanych według naszego modelu?
actPred = pd.DataFrame({"Actual": y_test, "Predicted": y_pred})
actPred
| Actual | Predicted | |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 2 | 0 | 1 |
| 3 | 1 | 1 |
| 4 | 0 | 0 |
| ... | ... | ... |
| 395 | 0 | 1 |
| 396 | 1 | 1 |
| 397 | 0 | 1 |
| 398 | 0 | 1 |
| 399 | 1 | 0 |
400 rows × 2 columns
actPred.loc[indexes,:]
| Actual | Predicted | |
|---|---|---|
| 0 | 1 | 1 |
| 2 | 0 | 1 |
| 8 | 1 | 1 |
| 26 | 1 | 1 |
| 80 | 1 | 0 |
| 242 | 0 | 0 |
O! Bardzo ładnie wyszło:
for which in indexes:
print("ID =", which)
ps = explainer.predict_surrogate(X_test.iloc[which,:], type = "lime")
ps.show_in_notebook()
ID = 0
ID = 2
ID = 8
ID = 26
ID = 80
ID = 242
Najpierw przyjrzyjmy się indeksom dobrze zaklasyfikowanym przez model:
Co do wszystkich powyższych win model był w miarę pewny - przy pewnościach między 86% a 97% otrzymaliśmy poprawny wynik. Czy podobnie było w przypadkach wypadku przy pracy naszego XGBoost-a?
🍷 LIME to metoda zdecydowanie godna uwagi. Jej ogromnym plusem jest fakt, że możemy odczytać, jak bardzo możemy być pewni co do predykcji i dlaczego. Satysfakcjonujące efekty tego widzimy na przykładzie 6 wziętych pod lupę powyższych obserwacji - okazuje się, że przypadek, co do którego LIME miał największe wątpliwości, był jednym z 2 z 6 win, które zostały źle przewidziane w kwestii oceny przez ekspertów. Zapewne także i globalnie, większa "kontrowersyjność" co do oceny sprzyja częstszym myleniu się!
🍷 Z refleksji wyżej bezpośrednio dostajemy fajny wniosek - przy bardziej typowych obserwacjach dla obu klas mamy większą pewność co do wyniku i LIME nas o tym informuje. Gorzej z przykładami niestandardowymi, obfitymi w znaczną ilość cech typowych jednocześnie dla jednej, jak i drugiej klasy klasyfikacji.
🍷 Przeciwnie do Break Down i Shapley Values, tu w zależności od obserwacji możemy zobaczyć naprawdę różną kolejność kluczowości cech. W zależności od właściwości wierszy ramki danych, "rankingi ważności" różnią się o wiele bardziej niż w przypadku najnowszej poznanej na zajęciach metody.
🍷 Kwartyle rzeczywiście grają istotną rolę w owej metodzie.
🍷 Analizując LIME dobrze mieć pod ręką macierz korelacji zbioru danych, a także wartości kwartyli. Przynajmniej na początku oswajania z metodą i danymi owocuje to w jeszcze przejrzystsze i sprawniejsze zrozumienie jak działa model i dekompozycja.